iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0
Modern Web

從實務需求出發:前端視覺與 UI 互動開發實踐系列 第 5

【Day 05】 同場加映:圖片 RWD

  • 分享至 

  • xImage
  •  

單看標題,是不是會想這有什麼好講的?就在圖片加上 width: 100%; height:100%; object-fit: cover 不就好了。

是這樣沒錯,但不是這樣的。

先來段小劇場

( ・∀・)σ < 這邊要顯示使用者自行上傳的圖片,圖片會完整呈現。

    好,沒問題啊!> d(・∀・ )

( ・∀・)σ < 圖片要加個圓角跟框框。

    好,沒問題啊!但會有空白喔(object-fit: contain)> d(・∀・ )

( ‘・ω・ˋ)σ < 我想讓邊框貼齊圖片邊緣耶。

    好…沒問題啊!我試試看!> d(・∀・ )

=========== A Few Minutes Later ===========

發現還要兼顧 RWD,不超出外容器的同時,還要置中。表情逐漸母湯 → (゚д ゚ ; )

.

動腦時間

問題分析

一開始是個很簡單的畫面需求,為什麼到實際製作時,會變成很複雜呢?首先,來看看有哪些要克服的問題點。

  • 圖片是由使用者上傳,所以無法先設定好圖片的寬度、高度、比例。
  • 外容器是 Day 04 範例中的 preview 區塊,會隨視窗變換寬高。除此之外,區塊內還有一個動態增減的選項 list。
  • 邊框要切齊圖片,並且要呈現圖片內容。無法直接在外容器使用 object-fit,因為 contain 會留空白、cover 有機會大片裁切圖片。
  • 圖片會用到 Flex 來置中,但是如果沒有給圖片設定尺寸的話,會依內容尺寸被壓縮。
  • 圖片的寬高不能超過容器。但至少要有一邊貼齊容器,好顯示圖片內容。

材料準備

藉由上方的分析,我們大概知道需要克服哪些事情,以及製作時需要哪些資訊,包含:

  1. 圖片的可用空間
  2. 圖片本身的寬與高
  3. 由上面兩項計算而來的寬高比

💡 今天的實作會使用到 Vue。考慮到讀者可能對 Vue 比較不熟悉,簡單介紹一下使用到的語法。

  • ref(script 內):建立響應式變數,用 .value 讀寫,變動時畫面自動更新。
  • ref="x"(template 內):取得該 DOM 元素,存入同名的 ref 變數。
  • ::屬性綁定,把變數或運算式傳給屬性,如 :src:style
  • @:事件監聽,如 @click@load

動手做

首先是 HTML 結構,需要三層:

  1. 第一層:量測用圖片的「可用空間」,會使用 ResizeObserver 監聽空間的變化
  2. 第二層:會被縮放的「視覺框」,能在這層做圓角和邊框
  3. 第三層:上傳圖片本身,提供原始比例
<template>
    <div>
        <!-- 第一層 -->
        <section class="image-container" ref="imageContainerRef">
            <!-- 第二層 -->
            <div class="image-frame" :style="frameStyle">
                <!-- 第三層 -->
                <img :src="imageUrl" alt="示範圖片" ref="imgElementRef" @load="onImageLoad" />
            </div>
        </section>
        ...
    </div>
</template>

然後,設定 style

/* 第一層:填滿可用空間,flex 置中 */
.image-container {
    width: 100%;
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
}

/* 第二層:外觀設定 */
.image-frame {
    overflow: hidden;
    border: 2px solid #fff;
    border-radius: 16px;
    box-shadow: 0 4px 14px rgb(0 0 0 / 0.25);
    transition: width 0.25s ease-in-out, height 0.25s ease-in-out;
}

/* 第三層:填滿 frame;因為 frame 已與圖片同比例,所以 cover 不會裁切 */
.image-frame img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

接下來,重點來了!

我們要用 JS 來計算、監聽畫面上的 DOM,並且將計算結果寫入 .image-frame 那層的 style 屬性中。考慮到 resize 的過渡,寬高設定是用 % 為單位。這樣在 debounce 重算前,框也不會超出容器。

import { ref, onMounted, onUnmounted } from 'vue'

const imageContainerRef = ref(null)
const imgElementRef = ref(null)
const frameStyle = ref({ width: '80%', height: '80%' })
const imageUrl = ref('上傳圖片的連結')

// 將計算的 px 轉成相對單位
const pxToPercent = (value, denominator) => (value / denominator) * 100 + '%'

// 核心 function
const calculateSize = () => {
    const img = imgElementRef.value
    const container = imageContainerRef.value
    if (!img || !container) return

		// 取得圖片的原始寬高,以及 container 的寬高
    const { naturalWidth: imgW, naturalHeight: imgH } = img
    const { clientWidth: containerW, clientHeight: containerH } = container
    
    if (!containerW || !containerH || !imgW || !imgH) return

		// 算出原始寬高比與容器寬高比
    const imgRatio = imgW / imgH
    const containerRatio = containerW / containerH

    if (imgRatio > containerRatio) {
        // 圖片比容器「更寬」:寬度撐滿,高度依比例縮小
        frameStyle.value.width = '100%'
        frameStyle.value.height = pxToPercent(containerW / imgRatio, containerH)
    } else {
        // 圖片比容器「更窄」:高度撐滿,寬度依比例縮小
        frameStyle.value.width = pxToPercent(containerH * imgRatio, containerW)
        frameStyle.value.height = '100%'
    }
}

// 圖片載入(含切換圖片)後立即計算一次
const onImageLoad = () => calculateSize()

// 加上 debounce 與監聽容器大小變動
const debounce = (fn, delay) => {
    let timer = null
    return (...args) => {
        clearTimeout(timer)
        timer = setTimeout(() => fn(...args), delay)
    }
}
const debouncedCalculateSize = debounce(calculateSize, 150)

let resizeObserver = null
onMounted(() => {
    resizeObserver = new ResizeObserver(debouncedCalculateSize)
    resizeObserver.observe(imageContainerRef.value)
})
onUnmounted(() => resizeObserver?.disconnect())

這樣就完成啦!是不是非常簡單呢?

.

.

.

其實,我有點驚訝。隨著文章到收尾階段,心中冒出「就這樣?!」的驚嘆。

當初開發的時候,花了非常多心思在思考如何處理畫面。踩了很多坑,多次調整寫法。現在拿出來再看,怎麼這麼簡單?!冷靜後仔細一想,這是因為文章一開始就把會碰到的問題先列出來。由問題出發,就能夠更清楚該怎麼規劃,像是三層的 HTML 結構、frame 的計算方式等等。

這次藉由參加鐵人賽,回顧之前寫的 code。在意料之外的地方也獲得了收穫。

明天,我們將進入新的子題目 — SVG。

つづく


上一篇
【Day 04】 Grid 實作:比起綠豆糕,我倒覺得像格子
系列文
從實務需求出發:前端視覺與 UI 互動開發實踐5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言